using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;

namespace CompanyX.TechnicalArchitecture.Factories
{
    /// <summary>
    /// Cache container for provider factory
    /// </summary>
    internal class ProviderAssemblyCacheItem
    {
        private Assembly providerAssembly;
        public Assembly ProviderAssembly
        {
            get { return providerAssembly; }
            set { providerAssembly = value; }
        }

        private Dictionary<string, object> providerCache = new Dictionary<string, object>(8);
        public Dictionary<string, object> ProviderCache
        {
            get { return providerCache; }
        }
    }

    /// <summary>
    /// Provider factory is used to dynamically load and execute a service provider
    /// </summary>
    public class ProviderFactory
    {
        /// <summary>
        /// On-demand cache of provider assemblies that are successfully loaded
        /// </summary>
        internal static Dictionary<string, ProviderAssemblyCacheItem> providerAssemblyCache = new Dictionary<string, ProviderAssemblyCacheItem>(8);

        /// <summary>
        /// Loads the specified service provider. Assembly containing the provider must be strong named
        /// and registered in the GAC or accessible to runtime via &lt;codebase&gt; configuration. Calling this
        /// repeatedly for the same assembly is not necessary since assemblies are cached on successful load
        /// </summary>
        /// <param name="providerInfo">Service provider information</param>
        public static void LoadProvider(AssemblyName providerInfo)
        {
            try
            {
                ProviderAssemblyCacheItem providerAssemblyInfo = null;

                // Optimistic check
                if (!providerAssemblyCache.TryGetValue(providerInfo.FullName, out providerAssemblyInfo))
                {
                    // Only one thread should load the assembly
                    lock (providerAssemblyCache)
                    {
                        // Check cache again because another thread could have added the assembly
                        // while the currrent one was blocked
                        if (!providerAssemblyCache.TryGetValue(providerInfo.FullName, out providerAssemblyInfo))
                        {
                            providerAssemblyInfo = new ProviderAssemblyCacheItem();
                            providerAssemblyInfo.ProviderAssembly = Assembly.Load(providerInfo);
                            providerAssemblyCache[providerInfo.FullName] = providerAssemblyInfo;
                        }
                    }
                }
            }
            catch (Exception /* e */)
            {
                // TODO: Log based on policy
                throw;
            }
        }

        /// <summary>
        /// Retrieves the assembly for the specified service provider
        /// </summary>
        /// <param name="providerInfo">Service provider information</param>
        /// <returns>Assembly containing the provider, null if not found</returns>
        public static Assembly GetProviderAssembly(AssemblyName providerInfo)
        {
            ProviderAssemblyCacheItem providerAssemblyInfo = null;
            if (providerAssemblyCache.TryGetValue(providerInfo.FullName, out providerAssemblyInfo))
                return providerAssemblyInfo.ProviderAssembly;

            return null;
        }

        /// <summary>
        /// Searches all loaded providers to locate and return an instance of the specified provider type. 
        /// Note the appropriate provider assemblies must be loaded prior to calling this else a null is returned
        /// </summary>
        /// <remarks>If there are multiple providers, the one returned is undeterministic</remarks>
        /// <typeparam name="T">Provider type to instantiate</typeparam>
        /// <returns>An instance of the specified provider</returns>
        public static T GetProvider<T>()
        {
            if (providerAssemblyCache.Count == 0)
                throw new Exception("No providers loaded");

            foreach (ProviderAssemblyCacheItem providerAssemblyInfo in providerAssemblyCache.Values)
            {
                foreach (Type provider in providerAssemblyInfo.ProviderAssembly.GetTypes())
                {
                    if (provider.GetInterface(typeof(T).Name) != null)
                        return (T)Activator.CreateInstance(provider);
                }
            }

            return default(T);
        }

        /// <summary>
        /// Searches a specific loaded provider to locate and return an instance of the specified provider type. 
        /// Note the specified provider assembly must be loaded prior to calling this else a null is returned
        /// </summary>
        /// <remarks>If there are multiple providers, the one returned is undeterministic</remarks>
        /// <typeparam name="T">Provider type to instantiate</typeparam>
        /// <param name="providerInfo">Specified provider</param>
        /// <returns>An instance of the specified provider</returns>
        public static T GetProvider<T>(AssemblyName providerInfo)
        {
            if (providerAssemblyCache.Count == 0)
                throw new Exception("No providers loaded");

            Assembly providerAssembly = providerAssemblyCache[providerInfo.FullName].ProviderAssembly;
            foreach (Type provider in providerAssembly.GetTypes())
            {
                if (provider.GetInterface(typeof(T).Name) != null)
                    return (T)Activator.CreateInstance(provider);
            }

            return default(T);
        }

        /// <summary>
        /// Searches a specific loaded provider to locate and return an instance of the specified provider type. 
        /// Note the specified provider assembly must be loaded prior to calling this else a null is returned
        /// </summary>
        /// <remarks>If there are multiple providers, the one returned is undeterministic</remarks>
        /// <typeparam name="T">Provider type to instantiate</typeparam>
        /// <param name="providerInfo">Specified provider</param>
        /// <param name="providerTypeName">Specified provider type full name</param>
        /// <returns>An instance of the specified provider</returns>
        public static T GetProvider<T>(AssemblyName providerInfo, string providerTypeName)
        {
            if (providerAssemblyCache.Count == 0)
                throw new Exception("No providers loaded");

            Assembly providerAssembly = providerAssemblyCache[providerInfo.FullName].ProviderAssembly;
            Type provider = providerAssembly.GetType(providerTypeName, false);
            if (provider != null)
            {
                if (provider.GetInterface(typeof(T).Name) != null)
                    return (T)Activator.CreateInstance(provider);
            }

            return default(T);
        }
    }
}
